package org.kits.service;

import java.util.HashMap;
import java.util.Map;

/**
 * 漏斗限流器：漏洞的容量是有限的，如果将漏嘴堵住，然后一直往里面灌水，它就会变满，直至再也
 * 装不进去。如果将漏嘴放开，水就会往下流，流走一部分之后，就又可以继续往里面灌水。
 * 如果漏嘴流水的速率大于灌水的速率，那么漏斗永远都装不满。如果漏嘴流水速率小于灌水
 * 的速率，那么一旦漏斗满了，灌水就需要暂停并等待漏斗腾空
 * 另外，Redis-Cell
 * Redis 4.0 提供了一个限流 Redis 模块，它叫 redis-cell。该模块也使用了漏斗算法，并
 * 提供了原子的限流指令。有了这个模块，限流问题就非常简单了。
 * @author SKyOne
 * @create 2020-03-08 19:47
 */
public class FlunnelRateLimiter {

    static class Flunnel{
        int capacity;       // 漏斗容量
        float leakingRate;  // 漏嘴流水速率
        int leftQuota;      // 漏斗剩余空间
        long leakingTs;     // 上一次漏水时间

        public Flunnel(int capacity,float leakingRate){
            this.capacity = capacity;
            this.leakingRate = leakingRate;
            this.leftQuota = capacity;
            this.leakingTs = System.currentTimeMillis();
        }

        // 计算腾出多少空间
        void makeSpace(){
            long nowts = System.currentTimeMillis();
            long deltaTs = nowts - leakingTs;   // 距离上一次漏水过去了多久
            int deltaQuota = (int)(deltaTs*leakingRate);    // 又可以腾出不少空间了
            // 间隔时间太长，整数数字过大溢出
            if(deltaQuota<0){
                this.leftQuota = capacity;
                this.leakingTs = nowts;
                return;
            }

            // 腾出空间太小，最小单位时1
            if(deltaQuota<1){
                return;
            }

            // 增加剩余空间
            this.leftQuota += deltaQuota;
            // 记录漏水时间
            this.leakingTs=nowts;
            if(this.leftQuota>this.capacity){
                this.leftQuota = this.capacity;
            }
        }

        /**
         * 开始灌水
         * @param quota
         * @return
         */
        boolean watering(int quota){
            makeSpace();
            if(this.leftQuota >= quota){
                this.leftQuota -= quota;
                return true;
            }
            return false;
        }
    }

    private Map<String,Flunnel> funnels = new HashMap<>();

    public boolean isActionAllowed(String userId,String actionKey,int capacity,float leakingRate){
        String key = String.format("%s:%s",userId,actionKey);
        Flunnel flunnel = funnels.get(key);
        if(null == flunnel){
            flunnel = new Flunnel(capacity,leakingRate);
            funnels.put(key,flunnel);
        }
        return flunnel.watering(1);
    }

}
